diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-25 03:28:27 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-25 03:28:27 +0000 |
| commit | 4c2d4c235bd80368e31cae9c375e9a585f6a6844 (patch) | |
| tree | 7fd1847e1e30ef2052281453bfb7a1c45ac6627a /app/api/projects/[projectId] | |
| parent | f69e125f1a0b47bbc22e2784208bf829bcdd24f8 (diff) | |
(대표님) archiver 추가, 데이터룸구현
Diffstat (limited to 'app/api/projects/[projectId]')
| -rw-r--r-- | app/api/projects/[projectId]/access/route.ts | 36 | ||||
| -rw-r--r-- | app/api/projects/[projectId]/members/[memberId]/route.ts | 89 | ||||
| -rw-r--r-- | app/api/projects/[projectId]/members/route.ts | 76 | ||||
| -rw-r--r-- | app/api/projects/[projectId]/route.ts | 134 | ||||
| -rw-r--r-- | app/api/projects/[projectId]/stats/route.ts | 275 |
5 files changed, 610 insertions, 0 deletions
diff --git a/app/api/projects/[projectId]/access/route.ts b/app/api/projects/[projectId]/access/route.ts new file mode 100644 index 00000000..c4b32ca8 --- /dev/null +++ b/app/api/projects/[projectId]/access/route.ts @@ -0,0 +1,36 @@ +// app/api/projects/[projectId]/access/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route' +import { ProjectService } from '@/lib/services/projectService'; + +// 프로젝트 접근 권한 확인 +export async function GET( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const projectService = new ProjectService(); + const access = await projectService.checkProjectAccess( + params.projectId, + Number(session.user.id) + ); + + return NextResponse.json({ + hasAccess: access.hasAccess, + role: access.role || 'viewer', + isOwner: access.isOwner, + }); + } catch (error) { + console.error('권한 확인 오류:', error); + return NextResponse.json( + { hasAccess: false, role: 'viewer', isOwner: false }, + { status: 500 } + ); + } +} diff --git a/app/api/projects/[projectId]/members/[memberId]/route.ts b/app/api/projects/[projectId]/members/[memberId]/route.ts new file mode 100644 index 00000000..55816661 --- /dev/null +++ b/app/api/projects/[projectId]/members/[memberId]/route.ts @@ -0,0 +1,89 @@ +// app/api/projects/[projectId]/members/[memberId]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route' +import { ProjectService } from '@/lib/services/projectService'; + +// 멤버 역할 수정 +export async function PATCH( + request: NextRequest, + { params }: { params: { projectId: string; memberId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const { role } = await request.json(); + const projectService = new ProjectService(); + + // Owner 또는 Admin만 가능 + const access = await projectService.checkProjectAccess( + params.projectId, + session.user.id, + 'admin' + ); + + if (!access.hasAccess && !access.isOwner) { + return NextResponse.json( + { error: '멤버 역할을 변경할 권한이 없습니다' }, + { status: 403 } + ); + } + + // 멤버 역할 업데이트 + await projectService.updateMemberRole( + params.projectId, + params.memberId, + role + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('멤버 역할 변경 오류:', error); + return NextResponse.json( + { error: '역할 변경에 실패했습니다' }, + { status: 500 } + ); + } +} + +// 멤버 제거 +export async function DELETE( + request: NextRequest, + { params }: { params: { projectId: string; memberId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const projectService = new ProjectService(); + + // Owner만 멤버 제거 가능 + const isOwner = await projectService.isProjectOwner( + params.projectId, + session.user.id + ); + + if (!isOwner) { + return NextResponse.json( + { error: '멤버를 제거할 권한이 없습니다' }, + { status: 403 } + ); + } + + // 멤버 제거 + await projectService.removeMember(params.projectId, params.memberId); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('멤버 제거 오류:', error); + return NextResponse.json( + { error: '멤버 제거에 실패했습니다' }, + { status: 500 } + ); + } +}
\ No newline at end of file diff --git a/app/api/projects/[projectId]/members/route.ts b/app/api/projects/[projectId]/members/route.ts new file mode 100644 index 00000000..d24b61e3 --- /dev/null +++ b/app/api/projects/[projectId]/members/route.ts @@ -0,0 +1,76 @@ +// app/api/projects/[projectId]/members/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route' +import { ProjectService } from '@/lib/services/projectService'; + +// 프로젝트 멤버 추가 (Owner만 가능) +export async function POST( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const body = await request.json(); + const projectService = new ProjectService(); + + await projectService.addProjectMember( + params.projectId, + body.userId, + body.role, + Number(session.user.id) + ); + + return NextResponse.json({ success: true }); + } catch (error: any) { + if (error.message.includes('소유자')) { + return NextResponse.json( + { error: error.message }, + { status: 403 } + ); + } + + console.error('멤버 추가 오류:', error); + return NextResponse.json( + { error: '멤버 추가에 실패했습니다' }, + { status: 500 } + ); + } +} + +export async function GET( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const projectService = new ProjectService(); + + const member = await projectService.getProjectMembers( + params.projectId, + ); + + return NextResponse.json({member}); + } catch (error: any) { + if (error.message.includes('소유자')) { + return NextResponse.json( + { error: error.message }, + { status: 403 } + ); + } + + console.error('멤버 조회 오류:', error); + return NextResponse.json( + { error: '멤버 조회에 실패했습니다' }, + { status: 500 } + ); + } +}
\ No newline at end of file diff --git a/app/api/projects/[projectId]/route.ts b/app/api/projects/[projectId]/route.ts new file mode 100644 index 00000000..38c11930 --- /dev/null +++ b/app/api/projects/[projectId]/route.ts @@ -0,0 +1,134 @@ +// app/api/projects/[projectId]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route' +import { ProjectService } from '@/lib/services/projectService'; +import { z } from 'zod'; + +// GET: 프로젝트 정보 조회 +export async function GET( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const projectService = new ProjectService(); + + // 프로젝트 접근 권한 확인 + const access = await projectService.checkProjectAccess( + params.projectId, + Number(session.user.id) + ); + + if (!access.hasAccess) { + return NextResponse.json( + { error: '프로젝트에 접근할 수 없습니다' }, + { status: 403 } + ); + } + + // 프로젝트 정보 가져오기 + const project = await projectService.getProject(params.projectId); + + if (!project) { + return NextResponse.json( + { error: '프로젝트를 찾을 수 없습니다' }, + { status: 404 } + ); + } + + // 사용자의 역할과 함께 프로젝트 정보 반환 + return NextResponse.json({ + ...project, + role: access.role, + isOwner: access.isOwner, + }); + } catch (error) { + console.error('프로젝트 조회 오류:', error); + return NextResponse.json( + { error: '프로젝트 정보를 불러올 수 없습니다' }, + { status: 500 } + ); + } +} + +// PATCH: 프로젝트 정보 수정 +export async function PATCH( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const body = await request.json(); + const projectService = new ProjectService(); + + // Admin 이상 권한 확인 + const access = await projectService.checkProjectAccess( + params.projectId, + Number(session.user.id), + 'admin' + ); + + if (!access.hasAccess) { + return NextResponse.json( + { error: '프로젝트를 수정할 권한이 없습니다' }, + { status: 403 } + ); + } + + await projectService.updateProjectSettings( + params.projectId, + Number(session.user.id), + body + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('프로젝트 수정 오류:', error); + return NextResponse.json( + { error: '프로젝트 수정에 실패했습니다' }, + { status: 500 } + ); + } +} + +// DELETE: 프로젝트 삭제 +export async function DELETE( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const projectService = new ProjectService(); + + // Owner만 삭제 가능 + await projectService.deleteProject(params.projectId, session.user.id); + + return NextResponse.json({ success: true }); + } catch (error: any) { + if (error.message.includes('소유자')) { + return NextResponse.json( + { error: error.message }, + { status: 403 } + ); + } + + console.error('프로젝트 삭제 오류:', error); + return NextResponse.json( + { error: '프로젝트 삭제에 실패했습니다' }, + { status: 500 } + ); + } +}
\ No newline at end of file diff --git a/app/api/projects/[projectId]/stats/route.ts b/app/api/projects/[projectId]/stats/route.ts new file mode 100644 index 00000000..dc2397ac --- /dev/null +++ b/app/api/projects/[projectId]/stats/route.ts @@ -0,0 +1,275 @@ +// app/api/fileSystemProjects/[projectId]/stats/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import db from "@/db/db"; +import { fileItems, fileActivityLogs, fileSystemProjects, projectMembers } from "@/db/schema"; +import { eq, and, gte, sql, desc } from "drizzle-orm"; + +export async function GET( + request: NextRequest, + context: { params: Promise<{ projectId: string }> } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const params = await context.params; + const projectId = params.projectId; + + // URL 파라미터에서 날짜 범위 가져오기 + const searchParams = request.nextUrl.searchParams; + const range = searchParams.get('range') || '30d'; + + // 날짜 범위 계산 + const now = new Date(); + let startDate = new Date(); + + switch (range) { + case '7d': + startDate.setDate(now.getDate() - 7); + break; + case '30d': + startDate.setDate(now.getDate() - 30); + break; + case '90d': + startDate.setDate(now.getDate() - 90); + break; + default: + startDate.setDate(now.getDate() - 30); + } + + // 이전 기간 (트렌드 계산용) + const previousStartDate = new Date(startDate); + previousStartDate.setDate(previousStartDate.getDate() - (now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); + + // 프로젝트 접근 권한 확인 + const projectMember = await db.query.projectMembers.findFirst({ + where: and( + eq(projectMembers.projectId, projectId), + eq(projectMembers.userId, Number(session.user.id)) + ), + }); + + const isInternalUser = session.user.domain !== 'partners'; + + // 내부 사용자가 아니고 프로젝트 멤버가 아닌 경우 접근 거부 + if (!isInternalUser && !projectMember) { + return NextResponse.json( + { error: '통계를 볼 권한이 없습니다' }, + { status: 403 } + ); + } + + // 1. 스토리지 통계 + const storageStats = await db + .select({ + totalSize: sql<number>`COALESCE(SUM(${fileItems.size}), 0)`, + fileCount: sql<number>`COUNT(CASE WHEN ${fileItems.type} = 'file' THEN 1 END)`, + folderCount: sql<number>`COUNT(CASE WHEN ${fileItems.type} = 'folder' THEN 1 END)`, + }) + .from(fileItems) + .where(eq(fileItems.projectId, projectId)); + + // 카테고리별 파일 수 + const categoryStats = await db + .select({ + category: fileItems.category, + count: sql<number>`COUNT(*)`, + }) + .from(fileItems) + .where(and( + eq(fileItems.projectId, projectId), + eq(fileItems.type, 'file') + )) + .groupBy(fileItems.category); + + const byCategory = { + public: 0, + restricted: 0, + confidential: 0, + internal: 0, + }; + + categoryStats.forEach(stat => { + if (stat.category && stat.category in byCategory) { + byCategory[stat.category as keyof typeof byCategory] = Number(stat.count); + } + }); + + // 2. 활동 통계 (현재 기간) + const activityStats = await db + .select({ + action: fileActivityLogs.action, + count: sql<number>`COUNT(*)`, + }) + .from(fileActivityLogs) + .where(and( + eq(fileActivityLogs.projectId, projectId), + gte(fileActivityLogs.createdAt, startDate) + )) + .groupBy(fileActivityLogs.action); + + // 이전 기간 통계 (트렌드 계산용) + const previousActivityStats = await db + .select({ + action: fileActivityLogs.action, + count: sql<number>`COUNT(*)`, + }) + .from(fileActivityLogs) + .where(and( + eq(fileActivityLogs.projectId, projectId), + gte(fileActivityLogs.createdAt, previousStartDate), + sql`${fileActivityLogs.createdAt} < ${startDate}` + )) + .groupBy(fileActivityLogs.action); + + const activityCounts = { + views: 0, + downloads: 0, + uploads: 0, + shares: 0, + }; + + const previousCounts = { + downloads: 0, + }; + + activityStats.forEach(stat => { + switch (stat.action) { + case 'view': + activityCounts.views = Number(stat.count); + break; + case 'download': + activityCounts.downloads = Number(stat.count); + break; + case 'upload': + activityCounts.uploads = Number(stat.count); + break; + case 'share': + activityCounts.shares = Number(stat.count); + break; + } + }); + + previousActivityStats.forEach(stat => { + if (stat.action === 'download') { + previousCounts.downloads = Number(stat.count); + } + }); + + // 트렌드 계산 (다운로드 기준) + const trend = previousCounts.downloads > 0 + ? Math.round(((activityCounts.downloads - previousCounts.downloads) / previousCounts.downloads) * 100) + : 0; + + // 3. 사용자 통계 + const userStats = await db + .select({ + total: sql<number>`COUNT(DISTINCT ${projectMembers.userId})`, + }) + .from(projectMembers) + .where(eq(projectMembers.projectId, projectId)); + + // 활성 사용자 (최근 활동이 있는 사용자) + const activeUsers = await db + .select({ + count: sql<number>`COUNT(DISTINCT ${fileActivityLogs.userId})`, + }) + .from(fileActivityLogs) + .where(and( + eq(fileActivityLogs.projectId, projectId), + gte(fileActivityLogs.createdAt, startDate) + )); + + // 역할별 사용자 수 (간단하게 처리) + const roleStats = await db + .select({ + role: projectMembers.role, + count: sql<number>`COUNT(*)`, + }) + .from(projectMembers) + .where(eq(projectMembers.projectId, projectId)) + .groupBy(projectMembers.role); + + const byRole = { + admin: 0, + editor: 0, + viewer: 0, + }; + + roleStats.forEach(stat => { + if (stat.role === 'manager') byRole.admin = Number(stat.count); + else if (stat.role === 'member') byRole.editor = Number(stat.count); + else byRole.viewer = Number(stat.count); + }); + + // 4. 최근 활동 내역 + const recentActivities = await db + .select({ + action: fileActivityLogs.action, + userEmail: fileActivityLogs.userEmail, + createdAt: fileActivityLogs.createdAt, + fileName: fileItems.name, + fileType: fileItems.type, + }) + .from(fileActivityLogs) + .leftJoin(fileItems, eq(fileActivityLogs.fileItemId, fileItems.id)) + .where(and( + eq(fileActivityLogs.projectId, projectId), + gte(fileActivityLogs.createdAt, startDate) + )) + .orderBy(desc(fileActivityLogs.createdAt)) + .limit(10); + + const recent = recentActivities.map(activity => ({ + type: activity.fileType || 'file', + user: activity.userEmail?.split('@')[0] || 'Unknown', + action: activity.action, + timestamp: activity.createdAt.toISOString(), + details: activity.fileName || 'Unknown file', + })); + + // 5. 프로젝트 정보 (스토리지 제한 등) + const project = await db.query.fileSystemProjects.findFirst({ + where: eq(fileSystemProjects.id, projectId), + }); + + const storageLimit = 10 * 1024 * 1024 * 1024; // 기본 10GB + + // 응답 데이터 구성 + const stats = { + storage: { + used: Number(storageStats[0]?.totalSize || 0), + limit: storageLimit, + fileCount: Number(storageStats[0]?.fileCount || 0), + folderCount: Number(storageStats[0]?.folderCount || 0), + byCategory, + }, + activity: { + views: activityCounts.views, + downloads: activityCounts.downloads, + uploads: activityCounts.uploads, + shares: activityCounts.shares, + trend, + }, + users: { + total: Number(userStats[0]?.total || 0), + active: Number(activeUsers[0]?.count || 0), + byRole, + }, + recent, + }; + + return NextResponse.json(stats); + + } catch (error) { + console.error('통계 조회 오류:', error); + return NextResponse.json( + { error: '통계를 불러올 수 없습니다' }, + { status: 500 } + ); + } +}
\ No newline at end of file |
